/*
 * Copyright 2009 Griffin Brown Digital Publishing Ltd
 * All rights reserved.
 *
 * This file is part of Probatron.
 *
 * Probatron is free software: you can redistribute it and/or modify
 * it under the terms of the Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Probatron is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * Affero General Public License for more details.
 *
 * You should have received a copy of the Affero General Public License
 * along with Probatron.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * This software contains, in modified form, source code originally created by 
 * the Jaxen Project.
 *
 * The copyright notice, conditions and disclaimer pertaining to that 
 * distribution are included below.
 *
 * Jaxen distributions are available from <http://jaxen.org/>.
 */  

/*
 * $Header:
 * /home/projects/jaxen/scm/jaxen/src/java/main/org/jaxen/saxpath/base/XPathReader.java,v 1.32
 * 2006/04/07 23:47:37 elharo Exp $ $Revision: 1.2 $ $Date: 2009/02/11 08:52:54 $
 * 
 * ====================================================================
 * 
 * Copyright 2000-2002 bob mcwhirter & James Strachan. All rights reserved.
 * 
 * 
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this list of
 * conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided
 * with the distribution.
 *  * Neither the name of the Jaxen Project nor the names of its contributors may be used to
 * endorse or promote products derived from this software without specific prior written
 * permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * ==================================================================== This software consists
 * of voluntary contributions made by many individuals on behalf of the Jaxen Project and was
 * originally created by bob mcwhirter <bob@werken.com> and James Strachan
 * <jstrachan@apache.org>. For more information on the Jaxen Project, please see
 * <http://www.jaxen.org/>.
 * 
 * @version $Id: XPathReader.java,v 1.2 2009/02/11 08:52:54 GBDP\andrews Exp $
 */

package org.probatron.jaxen.saxpath.base;

import java.util.ArrayList;

import org.probatron.jaxen.saxpath.Axis;
import org.probatron.jaxen.saxpath.Operator;
import org.probatron.jaxen.saxpath.SAXPathException;
import org.probatron.jaxen.saxpath.XPathHandler;
import org.probatron.jaxen.saxpath.XPathSyntaxException;
import org.probatron.jaxen.saxpath.helpers.DefaultXPathHandler;


/** Implementation of SAXPath's <code>XPathReader</code> which
 *  generates callbacks to an <code>XPathHandler</code>.
 *
 *  @author bob mcwhirter (bob@werken.com)
 */
public class XPathReader implements org.probatron.jaxen.saxpath.XPathReader
{
    private ArrayList tokens;
    private XPathLexer lexer;

    private XPathHandler handler;

    private static XPathHandler defaultHandler = new DefaultXPathHandler();


    /**
     * Create a new <code>XPathReader</code> with a do-nothing
     * <code>XPathHandler</code>.
     */
    public XPathReader()
    {
        setXPathHandler( defaultHandler );
    }


    public void setXPathHandler( XPathHandler handler )
    {
        this.handler = handler;
    }


    public XPathHandler getXPathHandler()
    {
        return this.handler;
    }


    public void parse( String xpath ) throws SAXPathException
    {
        setUpParse( xpath );

        getXPathHandler().startXPath();

        expr();

        getXPathHandler().endXPath();

        if( LA( 1 ) != TokenTypes.EOF )
        {
            XPathSyntaxException ex = createSyntaxException( "Unexpected '"
                    + LT( 1 ).getTokenText() + "'" );
            throw ex;
        }

        lexer = null;
        tokens = null;
    }


    void setUpParse( String xpath )
    {
        this.tokens = new ArrayList();
        this.lexer = new XPathLexer( xpath );
    }


    private void pathExpr() throws SAXPathException
    {
        getXPathHandler().startPathExpr();

        switch( LA( 1 ) ){
        case TokenTypes.DOUBLE:
        case TokenTypes.LITERAL: {
            filterExpr();

            if( LA( 1 ) == TokenTypes.SLASH || LA( 1 ) == TokenTypes.DOUBLE_SLASH )
            {
                XPathSyntaxException ex = createSyntaxException( "Node-set expected" );
                throw ex;
            }

            break;
        }
        case TokenTypes.LEFT_PAREN:
        case TokenTypes.DOLLAR: {
            filterExpr();

            if( LA( 1 ) == TokenTypes.SLASH || LA( 1 ) == TokenTypes.DOUBLE_SLASH )
            {
                locationPath( false );
            }
            break;
        }
        case TokenTypes.IDENTIFIER: {

            if( ( LA( 2 ) == TokenTypes.LEFT_PAREN && ! isNodeTypeName( LT( 1 ) ) )
                    || ( LA( 2 ) == TokenTypes.COLON && LA( 4 ) == TokenTypes.LEFT_PAREN ) )
            {
                filterExpr();

                if( LA( 1 ) == TokenTypes.SLASH || LA( 1 ) == TokenTypes.DOUBLE_SLASH )
                {
                    locationPath( false );
                }
            }
            else
            {
                locationPath( false );
            }
            break;
        }
        case TokenTypes.DOT:
        case TokenTypes.DOT_DOT:
        case TokenTypes.ANY_NAME:
        case TokenTypes.AT: {
            locationPath( false );
            break;
        }
        case TokenTypes.SLASH:
        case TokenTypes.DOUBLE_SLASH: {
            locationPath( true );
            break;
        }
        default: {
            XPathSyntaxException ex = createSyntaxException( "Unexpected '"
                    + LT( 1 ).getTokenText() + "'" );
            throw ex;
        }
        }

        getXPathHandler().endPathExpr();
    }


    private void literal() throws SAXPathException
    {
        Token token = match( TokenTypes.LITERAL );

        getXPathHandler().literal( token.getTokenText() );
    }


    private void functionCall() throws SAXPathException
    {
        String prefix = null;
        String functionName = null;

        if( LA( 2 ) == TokenTypes.COLON )
        {
            prefix = match( TokenTypes.IDENTIFIER ).getTokenText();
            match( TokenTypes.COLON );
        }
        else
        {
            prefix = "";
        }

        functionName = match( TokenTypes.IDENTIFIER ).getTokenText();

        getXPathHandler().startFunction( prefix, functionName );

        match( TokenTypes.LEFT_PAREN );

        arguments();

        match( TokenTypes.RIGHT_PAREN );

        getXPathHandler().endFunction();
    }


    private void arguments() throws SAXPathException
    {
        while( LA( 1 ) != TokenTypes.RIGHT_PAREN )
        {
            expr();

            if( LA( 1 ) == TokenTypes.COMMA )
            {
                match( TokenTypes.COMMA );
            }
            else
            {
                break;
            }
        }
    }


    private void filterExpr() throws SAXPathException
    {

        getXPathHandler().startFilterExpr();

        switch( LA( 1 ) ){
        case TokenTypes.DOUBLE: {
            Token token = match( TokenTypes.DOUBLE );

            getXPathHandler().number( Double.parseDouble( token.getTokenText() ) );
            break;
        }
        case TokenTypes.LITERAL: {
            literal();
            break;
        }
        case TokenTypes.LEFT_PAREN: {
            match( TokenTypes.LEFT_PAREN );
            expr();
            match( TokenTypes.RIGHT_PAREN );
            break;
        }
        case TokenTypes.IDENTIFIER: {
            functionCall();
            break;
        }
        case TokenTypes.DOLLAR: {
            variableReference();
            break;
        }
        }

        predicates();

        getXPathHandler().endFilterExpr();
    }


    private void variableReference() throws SAXPathException
    {
        match( TokenTypes.DOLLAR );

        String prefix = null;
        String variableName = null;

        if( LA( 2 ) == TokenTypes.COLON )
        {
            prefix = match( TokenTypes.IDENTIFIER ).getTokenText();
            match( TokenTypes.COLON );
        }
        else
        {
            prefix = "";
        }

        variableName = match( TokenTypes.IDENTIFIER ).getTokenText();

        getXPathHandler().variableReference( prefix, variableName );
    }


    void locationPath( boolean isAbsolute ) throws SAXPathException
    {
        switch( LA( 1 ) ){
        case TokenTypes.SLASH:
        case TokenTypes.DOUBLE_SLASH: {
            if( isAbsolute )
            {
                absoluteLocationPath();
            }
            else
            {
                relativeLocationPath();
            }
            break;
        }
        case TokenTypes.AT:
        case TokenTypes.IDENTIFIER:
        case TokenTypes.DOT:
        case TokenTypes.DOT_DOT:
        case TokenTypes.ANY_NAME: {
            relativeLocationPath();
            break;
        }
        default: {
            XPathSyntaxException ex = createSyntaxException( "Unexpected '"
                    + LT( 1 ).getTokenText() + "'" );
            throw ex;
        }
        }
    }


    private void absoluteLocationPath() throws SAXPathException
    {
        getXPathHandler().startAbsoluteLocationPath();

        switch( LA( 1 ) ){
        case TokenTypes.SLASH: {
            match( TokenTypes.SLASH );

            switch( LA( 1 ) ){

            case TokenTypes.DOT:
            case TokenTypes.DOT_DOT:
            case TokenTypes.AT:
            case TokenTypes.IDENTIFIER:
            case TokenTypes.ANY_NAME: {
                steps();
                break;
            }
            }
            break;
        }
        case TokenTypes.DOUBLE_SLASH: {
            getXPathHandler().startAllNodeStep( Axis.DESCENDANT_OR_SELF );
            getXPathHandler().endAllNodeStep();

            match( TokenTypes.DOUBLE_SLASH );
            switch( LA( 1 ) ){
            case TokenTypes.DOT:
            case TokenTypes.DOT_DOT:
            case TokenTypes.AT:
            case TokenTypes.IDENTIFIER:
            case TokenTypes.ANY_NAME: {
                steps();
                break;
            }
            default:
                XPathSyntaxException ex = this
                        .createSyntaxException( "Location path cannot end with //" );
                throw ex;
            }
            break;
        }
        }

        getXPathHandler().endAbsoluteLocationPath();
    }


    private void relativeLocationPath() throws SAXPathException
    {
        getXPathHandler().startRelativeLocationPath();

        switch( LA( 1 ) ){
        case TokenTypes.SLASH: {
            match( TokenTypes.SLASH );
            break;
        }
        case TokenTypes.DOUBLE_SLASH: {
            getXPathHandler().startAllNodeStep( Axis.DESCENDANT_OR_SELF );
            getXPathHandler().endAllNodeStep();

            match( TokenTypes.DOUBLE_SLASH );

            break;
        }
        }

        steps();

        getXPathHandler().endRelativeLocationPath();
    }


    private void steps() throws SAXPathException
    {
        switch( LA( 1 ) ){

        case TokenTypes.DOT:
        case TokenTypes.DOT_DOT:
        case TokenTypes.AT:
        case TokenTypes.IDENTIFIER:
        case TokenTypes.ANY_NAME: {
            step();
            break;
        }
        case TokenTypes.EOF: {
            return;
        }
        default: {
            XPathSyntaxException ex = createSyntaxException( "Expected one of '.', '..', '@', '*', <QName>" );
            throw ex;
        }
        }

        do
        {
            if( ( LA( 1 ) == TokenTypes.SLASH ) || ( LA( 1 ) == TokenTypes.DOUBLE_SLASH ) )
            {
                switch( LA( 1 ) ){
                case TokenTypes.SLASH: {
                    match( TokenTypes.SLASH );
                    break;
                }
                case TokenTypes.DOUBLE_SLASH: {
                    getXPathHandler().startAllNodeStep( Axis.DESCENDANT_OR_SELF );
                    getXPathHandler().endAllNodeStep();

                    match( TokenTypes.DOUBLE_SLASH );
                    break;
                }
                }
            }
            else
            {
                return;
            }

            switch( LA( 1 ) ){
            case TokenTypes.DOT:
            case TokenTypes.DOT_DOT:
            case TokenTypes.AT:
            case TokenTypes.IDENTIFIER:
            case TokenTypes.ANY_NAME: {
                step();
                break;
            }
            default: {
                XPathSyntaxException ex = createSyntaxException( "Expected one of '.', '..', '@', '*', <QName>" );
                throw ex;
            }
            }

        }
        while( true );
    }


    void step() throws SAXPathException
    {
        int axis = 0;

        switch( LA( 1 ) ){
        case TokenTypes.DOT:
        case TokenTypes.DOT_DOT: {
            abbrStep();
            return;
        }
        case TokenTypes.AT: {
            axis = axisSpecifier();
            break;
        }
        case TokenTypes.IDENTIFIER: {
            if( LA( 2 ) == TokenTypes.DOUBLE_COLON )
            {
                axis = axisSpecifier();
            }
            else
            {
                axis = Axis.CHILD;
            }
            break;
        }
        case TokenTypes.ANY_NAME: {
            axis = Axis.CHILD;
            break;
        }
        }

        nodeTest( axis );
    }


    private int axisSpecifier() throws SAXPathException
    {
        int axis = 0;

        switch( LA( 1 ) ){
        case TokenTypes.AT: {
            match( TokenTypes.AT );
            axis = Axis.ATTRIBUTE;
            break;
        }
        case TokenTypes.IDENTIFIER: {
            Token token = LT( 1 );

            axis = Axis.lookup( token.getTokenText() );

            if( axis == Axis.INVALID_AXIS )
            {
                throwInvalidAxis( token.getTokenText() );
            }

            match( TokenTypes.IDENTIFIER );
            match( TokenTypes.DOUBLE_COLON );

            break;
        }
        }

        return axis;
    }


    private void nodeTest( int axis ) throws SAXPathException
    {
        switch( LA( 1 ) ){
        case TokenTypes.IDENTIFIER: {
            switch( LA( 2 ) ){
            case TokenTypes.LEFT_PAREN: {
                nodeTypeTest( axis );
                break;
            }
            default: {
                nameTest( axis );
                break;
            }
            }
            break;
        }
        case TokenTypes.ANY_NAME: {
            nameTest( axis );
            break;
        }
        default:
            XPathSyntaxException ex = createSyntaxException( "Expected <QName> or *" );
            throw ex;
        }
    }


    private void nodeTypeTest( int axis ) throws SAXPathException
    {
        Token nodeTypeToken = match( TokenTypes.IDENTIFIER );
        String nodeType = nodeTypeToken.getTokenText();

        match( TokenTypes.LEFT_PAREN );

        if( "processing-instruction".equals( nodeType ) )
        {
            String piName = "";

            if( LA( 1 ) == TokenTypes.LITERAL )
            {
                piName = match( TokenTypes.LITERAL ).getTokenText();
            }

            match( TokenTypes.RIGHT_PAREN );

            getXPathHandler().startProcessingInstructionNodeStep( axis, piName );

            predicates();

            getXPathHandler().endProcessingInstructionNodeStep();
        }
        else if( "node".equals( nodeType ) )
        {
            match( TokenTypes.RIGHT_PAREN );

            getXPathHandler().startAllNodeStep( axis );

            predicates();

            getXPathHandler().endAllNodeStep();
        }
        else if( "text".equals( nodeType ) )
        {
            match( TokenTypes.RIGHT_PAREN );

            getXPathHandler().startTextNodeStep( axis );

            predicates();

            getXPathHandler().endTextNodeStep();
        }
        else if( "comment".equals( nodeType ) )
        {
            match( TokenTypes.RIGHT_PAREN );

            getXPathHandler().startCommentNodeStep( axis );

            predicates();

            getXPathHandler().endCommentNodeStep();
        }
        else
        {
            XPathSyntaxException ex = createSyntaxException( "Expected node-type" );
            throw ex;
        }
    }


    private void nameTest( int axis ) throws SAXPathException
    {
        String prefix = null;
        String localName = null;

        switch( LA( 2 ) ){
        case TokenTypes.COLON: {
            switch( LA( 1 ) ){
            case TokenTypes.IDENTIFIER: {
                prefix = match( TokenTypes.IDENTIFIER ).getTokenText();
                match( TokenTypes.COLON );
                break;
            }
            }
            break;
        }
        }

        switch( LA( 1 ) ){
        case TokenTypes.IDENTIFIER: {
            localName = match( TokenTypes.IDENTIFIER ).getTokenText();
            break;
        }
        case TokenTypes.ANY_NAME: {
            match( TokenTypes.ANY_NAME );
            localName = "*";
            break;
        }
        }

        if( prefix == null )
        {
            prefix = "";
        }

        getXPathHandler().startNameStep( axis, prefix, localName );

        predicates();

        getXPathHandler().endNameStep();
    }


    private void abbrStep() throws SAXPathException
    {
        switch( LA( 1 ) ){
        case TokenTypes.DOT: {
            match( TokenTypes.DOT );
            getXPathHandler().startAllNodeStep( Axis.SELF );
            predicates();
            getXPathHandler().endAllNodeStep();
            break;
        }
        case TokenTypes.DOT_DOT: {
            match( TokenTypes.DOT_DOT );
            getXPathHandler().startAllNodeStep( Axis.PARENT );
            predicates();
            getXPathHandler().endAllNodeStep();
            break;
        }
        }
    }


    private void predicates() throws SAXPathException
    {
        while( true )
        {
//            System.err.println( "predicates() " + LA( 1 ) + "="
//                    + TokenTypes.getTokenText( LA( 1 ) ) );
            if( LA( 1 ) == TokenTypes.LEFT_BRACKET )
            {
                predicate();
            }
            else
            {
                break;
            }
        }
    }


    void predicate() throws SAXPathException
    {
        getXPathHandler().startPredicate();

        match( TokenTypes.LEFT_BRACKET );

        predicateExpr();

        match( TokenTypes.RIGHT_BRACKET );

        getXPathHandler().endPredicate();
    }


    private void predicateExpr() throws SAXPathException
    {
        expr();
    }


    private void expr() throws SAXPathException
    {
        orExpr();
    }


    private void orExpr() throws SAXPathException
    {
        getXPathHandler().startOrExpr();

        andExpr();

        boolean create = false;

        switch( LA( 1 ) ){
        case TokenTypes.OR: {
            create = true;
            match( TokenTypes.OR );
            orExpr();
            break;
        }
        }

        getXPathHandler().endOrExpr( create );
    }


    private void andExpr() throws SAXPathException
    {
        getXPathHandler().startAndExpr();

        equalityExpr();

        boolean create = false;

        switch( LA( 1 ) ){
        case TokenTypes.AND: {
            create = true;
            match( TokenTypes.AND );
            andExpr();
            break;
        }
        }

        getXPathHandler().endAndExpr( create );
    }


    private void equalityExpr() throws SAXPathException
    {
        relationalExpr();

        int la = LA( 1 );
        while( la == TokenTypes.EQUALS || la == TokenTypes.NOT_EQUALS )
        {
            switch( la ){
            case TokenTypes.EQUALS: {
                match( TokenTypes.EQUALS );
                getXPathHandler().startEqualityExpr();
                relationalExpr();
                getXPathHandler().endEqualityExpr( Operator.EQUALS );
                break;
            }
            case TokenTypes.NOT_EQUALS: {
                match( TokenTypes.NOT_EQUALS );
                getXPathHandler().startEqualityExpr();
                relationalExpr();
                getXPathHandler().endEqualityExpr( Operator.NOT_EQUALS );
                break;
            }
            }
            la = LA( 1 );
        }
    }


    private void relationalExpr() throws SAXPathException
    {

        additiveExpr();

        int la = LA( 1 );
        // Very important: TokenTypes.LESS_THAN != Operator.LESS_THAN
        //                 TokenTypes.GREATER_THAN != Operator.GREATER_THAN
        //                 TokenTypes.GREATER_THAN_EQUALS != Operator.GREATER_THAN_EQUALS
        //                 TokenTypes.LESS_THAN_EQUALS != Operator.LESS_THAN_EQUALS
        while( la == TokenTypes.LESS_THAN_SIGN || la == TokenTypes.GREATER_THAN_SIGN
                || la == TokenTypes.LESS_THAN_OR_EQUALS_SIGN
                || la == TokenTypes.GREATER_THAN_OR_EQUALS_SIGN )
        {
            switch( la ){
            case TokenTypes.LESS_THAN_SIGN: {
                match( TokenTypes.LESS_THAN_SIGN );
                getXPathHandler().startRelationalExpr();
                additiveExpr();
                getXPathHandler().endRelationalExpr( Operator.LESS_THAN );
                break;
            }
            case TokenTypes.GREATER_THAN_SIGN: {
                match( TokenTypes.GREATER_THAN_SIGN );
                getXPathHandler().startRelationalExpr();
                additiveExpr();
                getXPathHandler().endRelationalExpr( Operator.GREATER_THAN );
                break;
            }
            case TokenTypes.GREATER_THAN_OR_EQUALS_SIGN: {
                match( TokenTypes.GREATER_THAN_OR_EQUALS_SIGN );
                getXPathHandler().startRelationalExpr();
                additiveExpr();
                getXPathHandler().endRelationalExpr( Operator.GREATER_THAN_EQUALS );
                break;
            }
            case TokenTypes.LESS_THAN_OR_EQUALS_SIGN: {
                match( TokenTypes.LESS_THAN_OR_EQUALS_SIGN );
                getXPathHandler().startRelationalExpr();
                additiveExpr();
                getXPathHandler().endRelationalExpr( Operator.LESS_THAN_EQUALS );
                break;
            }
            }
            la = LA( 1 );
        }
    }


    private void additiveExpr() throws SAXPathException
    {
        multiplicativeExpr();

        int la = LA( 1 );
        while( la == TokenTypes.PLUS || la == TokenTypes.MINUS )
        {
            switch( la ){
            case TokenTypes.PLUS: {
                match( TokenTypes.PLUS );
                getXPathHandler().startAdditiveExpr();
                multiplicativeExpr();
                getXPathHandler().endAdditiveExpr( Operator.ADD );
                break;
            }
            case TokenTypes.MINUS: {
                match( TokenTypes.MINUS );
                getXPathHandler().startAdditiveExpr();
                multiplicativeExpr();
                getXPathHandler().endAdditiveExpr( Operator.SUBTRACT );
                break;
            }
            }
            la = LA( 1 );
        }
    }


    private void multiplicativeExpr() throws SAXPathException
    {
        unaryExpr();

        int la = LA( 1 );
        while( la == TokenTypes.MULTIPLY || la == TokenTypes.DIV || la == TokenTypes.MOD )
        {
            switch( la ){
            case TokenTypes.MULTIPLY: {
                match( TokenTypes.MULTIPLY );
                getXPathHandler().startMultiplicativeExpr();
                unaryExpr();
                getXPathHandler().endMultiplicativeExpr( Operator.MULTIPLY );
                break;
            }
            case TokenTypes.DIV: {
                match( TokenTypes.DIV );
                getXPathHandler().startMultiplicativeExpr();
                unaryExpr();
                getXPathHandler().endMultiplicativeExpr( Operator.DIV );
                break;
            }
            case TokenTypes.MOD: {
                match( TokenTypes.MOD );
                getXPathHandler().startMultiplicativeExpr();
                unaryExpr();
                getXPathHandler().endMultiplicativeExpr( Operator.MOD );
                break;
            }
            }
            la = LA( 1 );
        }

    }


    private void unaryExpr() throws SAXPathException
    {
        switch( LA( 1 ) ){
        case TokenTypes.MINUS: {
            getXPathHandler().startUnaryExpr();
            match( TokenTypes.MINUS );
            unaryExpr();
            getXPathHandler().endUnaryExpr( Operator.NEGATIVE );
            break;
        }
        default: {
            unionExpr();
            break;
        }
        }

    }


    private void unionExpr() throws SAXPathException
    {
        getXPathHandler().startUnionExpr();

        pathExpr();

        boolean create = false;

        switch( LA( 1 ) ){
        case TokenTypes.PIPE: {
            match( TokenTypes.PIPE );
            create = true;
            expr();
            break;
        }
        }

        getXPathHandler().endUnionExpr( create );
    }


    private Token match( int tokenType ) throws XPathSyntaxException
    {
        LT( 1 );

        Token token = ( Token )tokens.get( 0 );

        if( token.getTokenType() == tokenType )
        {
            tokens.remove( 0 );
            return token;
        }

        XPathSyntaxException ex = createSyntaxException( "Expected: "
                + TokenTypes.getTokenText( tokenType ) );
        throw ex;
    }


    private int LA( int position )
    {
        return LT( position ).getTokenType();
    }


    // XXX This method's a HotSpot; could we improve it?
    private Token LT( int position )
    {
        if( tokens.size() <= ( position - 1 ) )
        {
            for( int i = 0; i < position; ++i )
            {
                Token token = lexer.nextToken();
                tokens.add( token );
            }
        }

        return ( Token )tokens.get( position - 1 );
    }


    private boolean isNodeTypeName( Token name )
    {
        String text = name.getTokenText();

        if( "node".equals( text ) || "comment".equals( text ) || "text".equals( text )
                || "processing-instruction".equals( text ) )
        {
            return true;
        }

        return false;
    }


    private XPathSyntaxException createSyntaxException( String message )
    {
        String xpath = this.lexer.getXPath();
        int position = LT( 1 ).getTokenBegin();

        return new XPathSyntaxException( xpath, position, message );
    }


    private void throwInvalidAxis( String invalidAxis ) throws SAXPathException
    {
        String xpath = this.lexer.getXPath();
        int position = LT( 1 ).getTokenBegin();

        String message = "Expected valid axis name instead of [" + invalidAxis + "]";

        throw new XPathSyntaxException( xpath, position, message );
    }
}
